package jmine.tec.test.junit4;

import static junit.framework.Assert.fail;

import java.util.concurrent.Callable;

import jmine.tec.environment.db.DBEnvironment;
import jmine.tec.environment.db.DBEnvironmentHolder;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.reflection.AnnotationUtils;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.transaction.TransactionStatus;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.SystemEntityIdentifier;
import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.dao.SystemEntityDAO;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType;

/**
 * Classe base para testes de banco de dados.
 * 
 * @author lundberg
 */
public abstract class DBTestCase {

    private static boolean firstTest;

    private TransactionStatus txStatus;

    private AuthorizationStatus previousAuthorizationStatus;

    /**
     * Marca que o próximo teste será o primeiro a ser executado.
     */
    @BeforeClass
    public static void markFirstTest() {
        firstTest = true;
    }

    /**
     * Stores the previous authorization status
     */
    @Before
    public void storePreviousAuthorizationStatus() {
        this.previousAuthorizationStatus = getEnvironment().getHelper().getThreadAuthorizationStatus();
    }

    /**
     * Disables authorization
     */
    public void disableAuthorization() {
        getEnvironment().getHelper().setThreadAuthorizationStatus(AuthorizationStatus.DISABLED);
    }

    /**
     * Restores the previous authorization status
     */
    @After
    public void restorePreviousAuthorizationStatus() {
        getEnvironment().getHelper().setThreadAuthorizationStatus(this.previousAuthorizationStatus);
    }

    /**
     * Marca que o primeiro teste já foi executado
     */
    @After
    public void markFirstTestGone() {
        firstTest = false;
    }

    /**
     * Reinicia o ambiente de banco, com o tipo de base de referência necessário, caso trate-se do primeiro teste. Abre transação para
     * acesso ao banco pelo teste.
     */
    @Before
    public void restartDBEnvironment() {
        if (this.isFirstTest()) {
            Tuple<Class<?>, DBEnv> tuple = AnnotationUtils.findClassAndAnnotationWithAnnotation(this.getClass(), DBEnv.class);
            if (tuple == null) {
                throw new RuntimeException("A classe de testes não possui anotação @DBEnv: " + this.getClass().getCanonicalName());
            }
            DBEnv env = tuple.tail();
            DBEnvironmentHolder.getInstance().instantiate(env.spring(), env.refdb());
            getEnvironment().setRefdbType(this.getReferenceDatabaseType());
            getEnvironment().restart();
            this.doExecuteTransacted(new Runnable() {
                public void run() {
                    DBTestCase.this.initializeTestData();
                }
            });

        }
        this.txStatus = getEnvironment().getHelper().getTransactionStatus();
    }

    /**
     * Initializes test data
     */
    protected void initializeTestData() {
        // Hook
    }

    /**
     * Efetua o rollback das alterações do banco feitas pelo teste na transação principal do teste.
     */
    @After
    public void rollbackDBChanges() {
        if (this.txStatus != null) {
            getEnvironment().getHelper().rollback(this.txStatus);
        }
    }

    /**
     * Verifica se o teste sendo rodado é o primeiro.
     * 
     * @return boolean
     */
    protected boolean isFirstTest() {
        return firstTest;
    }

    /**
     * Executa um bloco de código de forma transacionada. O banco de dados é automaticamente marcado como sujo.
     * 
     * @param runnable runnable
     */
    protected void executeTransacted(Runnable runnable) {
        try {
            this.doExecuteTransacted(runnable);
        } finally {
            getEnvironment().markDirty();
        }
    }

    /**
     * Executa um bloco de código de forma transacionada.
     * 
     * @param runnable runnable
     */
    private void doExecuteTransacted(Runnable runnable) {
        boolean success = false;
        TransactionStatus status = getEnvironment().getHelper().getTransactionStatus();
        try {
            runnable.run();
            success = true;
        } finally {
            if (success) {
                getEnvironment().getHelper().commit(status);
            } else {
                getEnvironment().getHelper().rollback(status);
            }
        }
    }

    /**
     * Pega algum bean do tipo requerido
     * 
     * @param <B> tipo requerido
     * @param requiredType requiredType
     * @return bean
     */
    protected static <B> B getBean(Class<B> requiredType) {
        ListableBeanFactory bf = (ListableBeanFactory) getEnvironment().getHelper().getBeanFactory();
        return requiredType.cast(BeanFactoryUtils.beanOfType(bf, requiredType));
    }

    /**
     * Obtém o bean com nome e tipo dadosrequerido
     * 
     * @param <B> tipo requerido
     * @param name name
     * @param requiredType requiredType
     * @return bean
     */
    protected static <B> B getBean(String name, Class<B> requiredType) {
        return requiredType.cast(getEnvironment().getHelper().getBeanFactory().getBean(name, requiredType));
    }

    /**
     * Obtém uma referência ao ambiente de banco de dados
     * 
     * @return DBEnvironment
     */
    protected static DBEnvironment getEnvironment() {
        return DBEnvironmentHolder.getInstance().getEnvironment();
    }

    /**
     * Obtém o tipo de base de referência utilizado pelo teste.
     * 
     * @return ReferenceDatabaseDescriptionType
     */
    protected ReferenceDatabaseDescriptionType getReferenceDatabaseType() {
        if (this.getClass().isAnnotationPresent(DBEnv.class)) {
            return this.getClass().getAnnotation(DBEnv.class).type();
        } else {
            return ReferenceDatabaseDescriptionType.SCHEMA;
        }
    }

    /**
     * @param <T> the Persistable type
     * @param clazz a {@link Persistable} class
     * @return a new instance of the specified type
     */
    protected <T extends Persistable> T createBean(Class<T> clazz) {
        return getBean(BaseDAOFactory.class).getGenericDAO(clazz).createBean();
    }

    /**
     * @param <T> a type of {@link Persistable}
     * @param clazz a {@link Persistable} class
     * @param nk a natural key
     * @return the given object OR null
     */
    protected <T extends Persistable> T findByNaturalKey(Class<T> clazz, String nk) {
        BaseDAO<T> dao = getBean(BaseDAOFactory.class).getGenericDAO(clazz);
        try {
            return dao.findByNaturalKey(nk);
        } catch (BeanNotFoundException e) {
            return null;
        }
    }

    /**
     * @param <T> a type of {@link Persistable}
     * @param <S> a type of {@link SystemEntityIdentifier}
     * @param daoClass a {@link SystemEntityDAO} class
     * @param si a {@link SystemEntityIdentifier}
     * @return the required object
     */
    protected <T extends Persistable, S extends SystemEntityIdentifier<?>> T findBySystemIdentifier(
            Class<? extends SystemEntityDAO<T, S>> daoClass, S si) {
        SystemEntityDAO<T, S> dao = getBean(BaseDAOFactory.class).getDAOByClass(daoClass);
        return dao.findBySystemIdentifier(si);
    }

    /**
     * Finds a cold entity by it's natural key
     * 
     * @param <T> entity type
     * @param persistableClass persistableClass
     * @param naturalKey naturalKey
     * @return entity
     */
    protected <T extends Persistable> T findColdByNaturalKey(final Class<T> persistableClass, final String naturalKey) {
        return this.executeCold(new Callable<T>() {
            public T call() throws Exception {
                return DBTestCase.this.findByNaturalKey(persistableClass, naturalKey);
            }
        });
    }

    /**
     * Executes the given block in a cold authorization context
     * 
     * @param callable callable
     * @return block result
     */
    protected <R> R executeCold(Callable<R> callable) {
        PartitionSessionFactory sessionFactory = getBean(PartitionSessionFactory.class);
        sessionFactory.changeSessionAuthorizationContext(sessionFactory.getCurrentSession(), AuthorizationContext.cold);
        try {
            return callable.call();
        } catch (Exception e) {
            fail(e.getMessage());
            throw new IllegalStateException(e);
        } finally {
            sessionFactory.changeSessionAuthorizationContext(sessionFactory.getCurrentSession(), AuthorizationContext.hot);
        }
    }
}